package org.infodavid.persistence.impl.hibernate.connection;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.dbcp.BasicDataSourceFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.HibernateException;
import org.hibernate.cfg.Environment;
import org.hibernate.service.jdbc.connections.spi.ConnectionProvider;

/**
 * <p>
 * A connection provider that uses an Apache commons DBCP connection pool.
 * </p>
 * <p>
 * To use this connection provider set:<br>
 * <code>hibernate.connection.provider_class&nbsp;org.hibernate.connection.DBCPConnectionProvider</code>
 * </p>
 *
 * <pre>
 * Supported Hibernate properties:
 *   hibernate.connection.driver_class
 *   hibernate.connection.url
 *   hibernate.connection.username
 *   hibernate.connection.password
 *   hibernate.connection.isolation
 *   hibernate.connection.autocommit
 *   hibernate.connection.pool_size
 *   hibernate.connection (JDBC driver properties)
 * </pre>
 *
 * <br>
 * All DBCP properties are also supported by using the hibernate.dbcp prefix. A complete list can be
 * found on the DBCP configuration page: <a
 * href="http://jakarta.apache.org/commons/dbcp/configuration.html"
 * >http://jakarta.apache.org/commons/dbcp/configuration.html</a>. <br>
 *
 * <pre>
 * Example:
 *   hibernate.connection.provider_class org.hibernate.connection.DBCPConnectionProvider
 *   hibernate.connection.driver_class org.hsqldb.jdbcDriver
 *   hibernate.connection.username sa
 *   hibernate.connection.password
 *   hibernate.connection.url jdbc:hsqldb:test
 *   hibernate.connection.pool_size 20
 *   hibernate.dbcp.initialSize 10
 *   hibernate.dbcp.maxWait 3000
 *   hibernate.dbcp.validationQuery select 1 from dual
 * </pre>
 * @see org.hibernate.connection.ConnectionProvider
 */
public class DbcpConnectionProvider implements ConnectionProvider {
  /** logger. */
  private static final Log LOGGER = LogFactory.getLog(DbcpConnectionProvider.class);

  /** The Constant PREFIX. */
  public static final String PREFIX = "hibernate.dbcp.";

  /** The Constant PROP_PASSWORD. */
  public final static String PROP_PASSWORD = "password";

  /** The Constant PROP_URL. */
  public final static String PROP_URL = "url";

  /** The Constant PROP_USERNAME. */
  public final static String PROP_USERNAME = "username";

  /** The Constant PROP_DRIVERCLASSNAME. */
  public final static String PROP_DRIVER_CLASS_NAME = "driverClassName";

  /** The Constant PROP_DEFAULTAUTOCOMMIT. */
  public final static String PROP_DEFAULT_AUTOCOMMIT = "defaultAutoCommit";

  /** The Constant PROP_DEFAULTREADONLY. */
  public final static String PROP_DEFAULT_READONLY = "defaultReadOnly";

  /** The Constant PROP_DEFAULTTRANSACTIONISOLATION. */
  public final static String PROP_DEFAULT_TRANSACTION_ISOLATION = "defaultTransactionIsolation";

  /** The Constant PROP_DEFAULTCATALOG. */
  public final static String PROP_DEFAULTCATALOG = "defaultCatalog";

  /** The Constant PROP_MAXACTIVE. */
  public final static String PROP_MAX_ACTIVE = "maxActive";

  /** The Constant PROP_MAXIDLE. */
  public final static String PROP_MAXIDLE = "maxIdle";

  /** The Constant PROP_MINIDLE. */
  public final static String PROP_MINIDLE = "minIdle";

  /** The Constant PROP_INITIALSIZE. */
  public final static String PROP_INITIALSIZE = "initialSize";

  /** The Constant PROP_MAXWAIT. */
  public final static String PROP_MAXWAIT = "maxWait";

  /** The Constant PROP_TESTONBORROW. */
  public final static String PROP_TEST_ON_BORROW = "testOnBorrow";

  /** The Constant PROP_TESTONRETURN. */
  public final static String PROP_TEST_ON_RETURN = "testOnReturn";

  /** The Constant PROP_TIMEBETWEENEVICTIONRUNSMILLIS. */
  public final static String PROP_TIME_BETWEEN_EVICTION_RUNS_MILLIS =
      "timeBetweenEvictionRunsMillis";

  /** The Constant PROP_NUMTESTSPEREVICTIONRUN. */
  public final static String PROP_NUM_TESTS_PER_EVICTION_RUN = "numTestsPerEvictionRun";

  /** The Constant PROP_MINEVICTABLEIDLETIMEMILLIS. */
  public final static String PROP_MIN_EVICTABLE_IDLE_TIME_MILLIS = "minEvictableIdleTimeMillis";

  /** The Constant PROP_TESTWHILEIDLE. */
  public final static String PROP_TEST_WHILE_IDLE = "testWhileIdle";

  /** The Constant PROP_VALIDATIONQUERY. */
  public final static String PROP_VALIDATION_QUERY = "validationQuery";

  /** The Constant PROP_VALIDATIONQUERY_TIMEOUT. */
  public final static String PROP_VALIDATION_QUERY_TIMEOUT = "validationQueryTimeout";

  /** The Constant PROP_REMOVE_ABANDONED. */
  public final static String PROP_REMOVE_ABANDONED = "removeAbandoned";

  /** The ds. */
  private BasicDataSource ds;

  public void configure(final Properties props) throws HibernateException {
    final boolean isDebugEnabled = LOGGER.isDebugEnabled();
    final Properties dbcpProperties = new Properties();
    Connection connection = null;

    if (isDebugEnabled) {
      LOGGER.debug("Configure DbcpConnectionProvider");
    }

    try {
      dbcpProperties.put(PROP_DRIVER_CLASS_NAME, props.getProperty(Environment.DRIVER));
      dbcpProperties.put(PROP_URL, props.getProperty(Environment.URL));
      dbcpProperties.put(PROP_USERNAME, props.getProperty(Environment.USER));
      dbcpProperties.put(PROP_PASSWORD, props.getProperty(Environment.PASS));
      dbcpProperties.put(PROP_REMOVE_ABANDONED, String.valueOf(Boolean.TRUE));

      // Isolation level
      final String isolationLevel = props.getProperty(Environment.ISOLATION);

      if (isolationLevel != null && isolationLevel.trim().length() > 0) {
        dbcpProperties.put(PROP_DEFAULT_TRANSACTION_ISOLATION, isolationLevel);
      }

      // Turn off autocommit (unless autocommit property is set)
      final String autocommit = props.getProperty(Environment.AUTOCOMMIT);

      if (autocommit != null && autocommit.trim().length() > 0) {
        dbcpProperties.put(PROP_DEFAULT_AUTOCOMMIT, autocommit);
      }
      else {
        dbcpProperties.put(PROP_DEFAULT_AUTOCOMMIT, String.valueOf(Boolean.FALSE));
      }

      // Pool size
      final String poolSize = props.getProperty(Environment.POOL_SIZE);

      if (poolSize != null && poolSize.trim().length() > 0 && Integer.parseInt(poolSize) > 0) {
        dbcpProperties.put(PROP_MAX_ACTIVE, poolSize);
      }

      // Copy all "driver" properties into "connectionProperties"
      final Properties driverProps = db.getConnectionProperties(props);

      if (!driverProps.isEmpty()) {
        final StringBuffer connectionProperties = new StringBuffer();
        final Iterator<Map.Entry<Object,Object>> ite = driverProps.entrySet().iterator();
        Map.Entry<Object,Object> entry;

        if (ite.hasNext()) {
          entry = ite.next();

          connectionProperties.append(entry.getKey()).append('=').append(entry.getValue());

          while (ite.hasNext()) {
            entry = ite.next();

            connectionProperties.append(';');
            connectionProperties.append(entry.getKey()).append('=').append(entry.getValue());
          }
        }

        dbcpProperties.put("connectionProperties", connectionProperties.toString());
      }

      // Copy all DBCP properties removing the prefix
      for (final Map.Entry<Object,Object> entry : props.entrySet()) {
        final String key = (String)entry.getKey();
        // System ,property can override the given properties
        final String value = System.getProperty(key);

        if (key.startsWith(PREFIX)) {
          dbcpProperties.put(key.substring(PREFIX.length()), value == null ? entry.getValue()
              : value);
        }
      }

      if (isDebugEnabled) {
        System.out.println("Creating a DBCP data source using the properties:");

        dbcpProperties.list(System.out);
      }

      // Let the factory create the pool
      ds = (BasicDataSource)BasicDataSourceFactory.createDataSource(dbcpProperties);

      LOGGER.info(toString(ds));

      // The BasicDataSource has lazy initialization
      // borrowing a connection will start the DataSource
      // and make sure it is configured correctly.
      connection = getConnection();
    }
    catch (final Exception e) {
      final String message = "Could not create a DBCP pool";

      LOGGER.fatal(message, e);

      if (ds != null) {
        try {
          ds.close();
        }
        catch (final Exception e2) {
          if (isDebugEnabled) {
            LOGGER.debug("An error occurs while closing DBCP datasource", e2);
          }
        }

        ds = null;
      }

      throw new HibernateException(message, e);
    }
    finally {
      if (connection != null) {
        try {
          connection.close();
        }
        catch (final Exception e) {
          if (isDebugEnabled) {
            LOGGER.debug("An error occurs while closing the connection", e);
          }
        }
      }
    }

    if (isDebugEnabled) {
      LOGGER.debug("Configuration of DbcpConnectionProvider completed");
    }
  }

  /*
   * (non-Javadoc)
   * @see org.hibernate.connection.ConnectionProvider#getConnection()
   */
  @Override
  public Connection getConnection() throws SQLException {
    Connection conn = null;

    try {
      conn = ds.getConnection();
    }
    catch (final Throwable e) {
      LOGGER.info("Dbcp pool settings:\n" + toString(ds));

      throw new SQLException("Cannot get a connection: " + ds.getUrl() + " using user: "
          + ds.getUsername() + " from Dbcp pool.", e);
    }
    finally {
      logStatistics("Getting connection");
    }

    return conn;
  }

  /*
   * (non-Javadoc)
   * @see org.hibernate.connection.ConnectionProvider#closeConnection(java.sql. Connection)
   */
  @Override
  public void closeConnection(final Connection conn) throws SQLException {
    try {
      conn.close();
    }
    finally {
      logStatistics("Closing connection");
    }
  }

  /*
   * (non-Javadoc)
   * @see org.hibernate.connection.ConnectionProvider#close()
   */
  public void close() throws HibernateException {
    logStatistics("Close DbcpConnectionProvider");

    try {
      if (ds != null) {
        ds.close();
        ds = null;
      }
      else {
        LOGGER.warn("Cannot close DBCP pool (not initialized)");
      }
    }
    catch (final Exception e) {
      throw new HibernateException("Could not close DBCP pool", e);
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Close DBCPConnectionProvider complete");
    }
  }

  /**
   * Log statistics.
   * @param msg the msg
   */
  protected void logStatistics(final String msg) {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("active: " + ds.getNumActive() + " (max: " + ds.getMaxActive() + ")   "
          + "idle: " + ds.getNumIdle() + "(max: " + ds.getMaxIdle() + ")");
    }
  }

  /*
   * (non-Javadoc)
   * @see org.hibernate.connection.ConnectionProvider#supportsAggressiveRelease()
   */
  @Override
  public boolean supportsAggressiveRelease() {
    return true;
  }

  /**
   * To string.
   * @param dataSource the data source
   * @return the string
   */
  public String toString(final BasicDataSource dataSource) {
    final StringBuffer sb = new StringBuffer(255);

    sb.append(dataSource.toString());
    sb.append('\n');
    sb.append("URL: ");
    sb.append(dataSource.getUrl());
    sb.append('\n');
    sb.append("User name: ");
    sb.append(dataSource.getUsername());
    sb.append('\n');
    sb.append("initial pool size: ");
    sb.append(dataSource.getInitialSize());
    sb.append('\n');
    sb.append("auto commit: ");
    sb.append(dataSource.getDefaultAutoCommit());
    sb.append('\n');
    sb.append("max active: ");
    sb.append(dataSource.getMaxActive());
    sb.append('\n');
    sb.append("max idle: ");
    sb.append(dataSource.getMaxIdle());
    sb.append('\n');
    sb.append("max wait: ");
    sb.append(dataSource.getMaxWait());
    sb.append('\n');
    sb.append("min idle: ");
    sb.append(dataSource.getMinIdle());
    sb.append('\n');
    sb.append("test on borrow: ");
    sb.append(dataSource.getTestOnBorrow());
    sb.append('\n');
    sb.append("test on return: ");
    sb.append(dataSource.getTestOnReturn());
    sb.append('\n');
    sb.append("test while idle: ");
    sb.append(dataSource.getTestWhileIdle());
    sb.append('\n');
    sb.append("validation query: ");
    sb.append(dataSource.getValidationQuery());
    sb.append('\n');
    sb.append("max opened statements: ");
    sb.append(dataSource.getMaxOpenPreparedStatements());
    sb.append('\n');

    return sb.toString();
  }
}
